/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.ObjectModel;

namespace DaveSexton.DocProject.DocSites
{
	[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Multi")]
	public class MultiQueryOperation : QueryOperation
	{
		#region Public Properties
		public sealed override IEnumerable<QueryExpression> Operands
		{
			get
			{
				foreach (QueryExpression expr in operands)
					yield return expr;
			}
		}

		public override int OperandCount
		{
			get
			{
				return operands.Count;
			}
		}

		public sealed override bool MissingOperand
		{
			get
			{
				if (operands.Count < 2)
					return true;

				foreach (QueryExpression expr in operands)
				{
					QueryOperation operation = expr as QueryOperation;

					if (operation != null && operation.MissingOperand)
						return true;
				}

				return false;
			}
		}

		public sealed override bool IsOptimized
		{
			get
			{
				return optimized || Operator.IsOptimized;
			}
		}
		#endregion

		#region Private / Protected
		protected IList<QueryExpression> OperandsList
		{
			get
			{
				return operands;
			}
		}

		private readonly List<QueryExpression> operands = new List<QueryExpression>(4);
		private readonly bool optimized;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="MultiQueryOperation" /> class.
		/// </summary>
		public MultiQueryOperation(QueryOperator op, params QueryExpression[] operands)
			: base(op)
		{
			if (operands == null)
				throw new ArgumentNullException("operands");

			foreach (QueryExpression expr in operands)
			{
				if (expr != null)
				{
					this.operands.Add(expr);
					expr.Parent = this;
				}
			}
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="MultiQueryOperation" /> class.
		/// </summary>
		public MultiQueryOperation(QueryExpression parent, QueryOperator op, params QueryExpression[] operands)
			: base(op, parent)
		{
			if (operands == null)
				throw new ArgumentNullException("operands");

			foreach (QueryExpression expr in operands)
			{
				if (expr != null)
				{
					this.operands.Add(expr);
					expr.Parent = this;
				}
			}
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="MultiQueryOperation" /> class.
		/// </summary>
		protected MultiQueryOperation(QueryOperator op, bool optimized, params QueryExpression[] operands)
			: base(op)
		{
			if (operands == null)
				throw new ArgumentNullException("operands");

			foreach (QueryExpression expr in operands)
			{
				if (expr != null && (!optimized || !(expr is EmptyQueryExpression)))
				{
					this.operands.Add(expr);
					expr.Parent = this;
				}
			}

			this.optimized = optimized;
		}
		#endregion

		#region Methods
		public override QueryExpression Normalize()
		{
			List<QueryExpression> normalized = new List<QueryExpression>(operands.Count);

			foreach (QueryExpression expr in operands)
			{
				if (expr != null && !(expr is EmptyQueryExpression))
				{
					QueryExpression result = expr.Normalize();

					if (result != null && !(result is EmptyQueryExpression))
						normalized.Add(result);
				}
			}

			if (normalized.Count == 0)
				return EmptyQueryExpression.Value;
			else
				return new MultiQueryOperation(Operator, IsOptimized, normalized.ToArray());
		}

		public override QueryExpression Optimize()
		{
			if (IsOptimized)
			{
				MultiQueryOperation op = new MultiQueryOperation(Operator, true, operands.ToArray());
				operands.Clear();
				return op;
			}

			switch (operands.Count)
			{
				case 0:
					return EmptyQueryExpression.Value;
				case 1:
					QueryExpression expr = operands[0];

					if (expr == null || expr is EmptyQueryExpression)
						return EmptyQueryExpression.Value;
					else
						return expr.Optimize();
			}

			// NOTE: call Optimize() on each operand regardless of whether IsOptimized is true
			// so that at least a new instance is returned.

			List<QueryExpression> allOptimized = new List<QueryExpression>(operands.Count * 2);
			List<string> uniqueTerms = new List<string>(operands.Count * 2);
			int unaryStartIndex = 0;
			bool ensureComplexExpressionsFirst = Operator.IsAssociative;

			foreach (QueryExpression expr in operands)
			{
				if (expr == null || expr is EmptyQueryExpression)
					continue;

				QueryExpression optimizedExpr = expr.Optimize();

				if (optimizedExpr == null || optimizedExpr is EmptyQueryExpression)
					continue;

				QueryOperation operation = optimizedExpr as QueryOperation;

				if (operation == null)
				{
					if (!IsDuplicateTerm(optimizedExpr, uniqueTerms, true))
						allOptimized.Add(optimizedExpr);
				}
				else if (operation.Operator == Operator)
				{
					if (ensureComplexExpressionsFirst)
					{
						int index = unaryStartIndex;
						allOptimized.InsertRange(index, GetUniqueExpressions(operation.Operands, uniqueTerms, delegate() { unaryStartIndex++; }));
					}
					else
						allOptimized.AddRange(GetUniqueExpressions(operation.Operands, uniqueTerms));
				}
				else if (ensureComplexExpressionsFirst)
				{
					if (!IsDuplicateTerm(optimizedExpr, uniqueTerms, true))
						allOptimized.Insert(unaryStartIndex, optimizedExpr);
				}
				else if (!IsDuplicateTerm(optimizedExpr, uniqueTerms, true))
					allOptimized.Add(optimizedExpr);
			}

			switch (allOptimized.Count)
			{
				case 0:
					return EmptyQueryExpression.Value;
				case 1:
					return allOptimized[0];
				default:
					return new MultiQueryOperation(Operator, true, allOptimized.ToArray());
			}
		}

		private static IEnumerable<QueryExpression> GetUniqueExpressions(IEnumerable<QueryExpression> expressions, List<string> uniqueTerms)
		{
			return GetUniqueExpressions(expressions, uniqueTerms, null);
		}

		private static IEnumerable<QueryExpression> GetUniqueExpressions(IEnumerable<QueryExpression> expressions, List<string> uniqueTerms,
			System.Threading.ThreadStart callback)
		{
			foreach (QueryExpression expr in expressions)
			{
				if (!IsDuplicateTerm(expr, uniqueTerms, true))
				{
					if (callback != null)
						callback();

					yield return expr;
				}
			}
		}

		private static bool IsDuplicateTerm(QueryExpression expression, List<string> uniqueTerms, bool addIfUnique)
		{
			TermQueryExpression termQuery = expression as TermQueryExpression;

			if (termQuery == null)
				return false;

			string term = termQuery.Term;

			bool exists = uniqueTerms.Exists(delegate(string match)
			{
				return string.Equals(term, match, StringComparison.CurrentCultureIgnoreCase);
			});

			if (addIfUnique && !exists)
				uniqueTerms.Add(term);

			return exists;
		}

		public override void AppendOperand(QueryExpression nextOperand)
		{
			if (nextOperand == null)
				throw new ArgumentNullException("nextOperand");
			else if (operands.Count < 2)
			{
				operands.Add(nextOperand);
				nextOperand.Parent = this;
			}
			else
			{
				for (int i = operands.Count - 1; i > -1; i--)
				{
					QueryOperation operation = operands[i] as QueryOperation;

					if (operation != null && operation.MissingOperand)
					{
						operation.AppendOperand(nextOperand);
						return;
					}
				}

				throw new InvalidOperationException(Resources.Errors.NoMissingOperands);
			}
		}

		public override void Evaluate(Action<QueryExpression> action)
		{
			foreach (QueryExpression expr in operands)
				expr.Evaluate(action);

			action(this);
		}

		public override void AppendToString(StringBuilder output)
		{
			output.Append('{');

			for (int i = 0; i < operands.Count; i++)
			{
				QueryExpression expr = operands[i];

				expr.AppendToString(output);

				if (i < operands.Count - 1)
					output.Append(" " + Operator + " ");
			}

			output.Append('}');
		}
		#endregion
	}
}
